#!/usr/bin/ruby
#
# Copyright:: Copyright 2012, Trimble Navigation Limited
# License:: All Rights Reserved.
# Original Author:: Scott Shattuck
#
# Overlay file used to support fast extension of dcclass_v1.rb without causing
# as many conflicts due to competing edits.

require "sketchup.rb"
require "langhandler.rb"

class DynamicComponentsV1

  # Alias original show_dialog so we can extend it without accessing the
  # original source code file
  alias show_dialog_orig show_dialog

  # Shows a WebDialog after creating it (if needed)
  #
  #    Args:
  #      dialog_name: required string, a unique, friendly name for the dialog
  #      dialog_url: optional string, the url to display
  #      dialog_w: optional int, width of the dialog in pixels
  #      dialog_h: optional int, height of the dialog in pixels
  #      dialog_x: optional int, screen x position of the dialog in pixels
  #      dialog_y: optional int, screen y position of the dialog in pixels
  #      is_modal: optional boolean, whether to show as a modal dialog
  def show_dialog(dialog_name, dialog_url = "http://www.google.com",
                               dialog_w = 1024, dialog_h = 668, dialog_x = 300, dialog_y = 300, is_modal = false,
                               clear_highlight_on_close = false)

    # invoke the original version so previous registrations are in place
    show_dialog_orig(dialog_name, dialog_url, dialog_w, dialog_h, dialog_x,
                     dialog_y, is_modal, clear_highlight_on_close)

    # define our new function(s) for dialogs
    @dialogs[dialog_name].add_action_callback("pull_selection_ids") do |d, p|
      pull_selection_ids(d, p)
    end
    @dialogs[dialog_name].add_action_callback("push_attribute_set") do |d, p|
      push_attribute_set(d, p)
    end
    @dialogs[dialog_name].add_action_callback("set_dialog_properties") do |d, p|
      set_dialog_properties(d, p)
    end
    @dialogs[dialog_name].add_action_callback("is_online") do |d, p|
      is_online(d, p)
    end
    @dialogs[dialog_name].add_action_callback("pull_information") do |d, p|
      pull_information(d, p)
    end
    @dialogs[dialog_name].add_action_callback("read_templates") do |d, p|
      read_templates(d, p)
    end
    @dialogs[dialog_name].add_action_callback("update_template") do |d, p|
      update_template(d, p)
    end
    @dialogs[dialog_name].add_action_callback("delete_template") do |d, p|
      delete_template(d, p)
    end
    @dialogs[dialog_name].add_action_callback("save_report") do |d, p|
      save_report(d, p)
    end
  end

  # Returns true if the application appears to be online, meaning it looks
  # like network connectivity exists.
  #
  # Args
  def is_online(dialog, data)
    json = '{"online":' + Sketchup.is_online.to_s + "}"
    js_callback(dialog, "is_online", data, json, nil)
  end

  # Pops open the configure dialog for a given selection
  #
  #   Args:
  #     None.
  #
  #   Returns:
  #     Nothing
  def show_configure_dialog()
    save_configure_thumbnail()

    show_dialog($dc_CONFIGURATOR_NAME,
                File.join($Ahtml目录, "configurator.html"),
                345, 560)
  end

  # Configures the dialog's properties (width, height, x, y, etc.) from any
  # key/value data provided.

  # Args:
  # - dialog: reference to the WebDialog that made the call
  # - data: string of the WebDialog parameters
  def set_dialog_properties(dialog, data)
    params = query_to_hash(dialog, data)
    width = second_if_empty(params["w"], params["width"]).to_i
    height = second_if_empty(params["h"], params["height"]).to_i

    # Bail if nothing was passed.
    if width == 0
      return
    end

    # Apply a minimum size to these dialogs.
    if width < MIN_REPORT_WIDTH
      width = MIN_REPORT_WIDTH
    end
    if width > MAX_REPORT_WIDTH
      width = MAX_REPORT_WIDTH
    end
    if height < MIN_REPORT_HEIGHT
      height = MIN_REPORT_HEIGHT
    end
    if height > MAX_REPORT_HEIGHT
      height = MAX_REPORT_HEIGHT
    end

    # Only update our size if its different from the current state. This
    # solves some problems where dialogs will refocus themselves when
    # resized.
    if width != nil && height != nil
      if width != dialog.last_width || height != dialog.last_height
        dialog.set_size width, height
        dialog.last_width = width
        dialog.last_height = height
      end
    end

    top = params["y"]
    left = params["x"]
    dialog.set_position top.to_i, left.to_i unless top.nil? || left.nil?
  end

  # Produces JSON-formatted text describing one or more entities. The entities
  # are drawn from a selection_ids key, or from the first element in the
  # current selection if no IDs are provided.
  #
  # Args:
  # - dialog: reference to the WebDialog that made the call
  # - data: string of the WebDialog params
  #
  # Returns:
  #   Nothing
  def pull_attribute_tree(dialog, data)
    # puts "pull_attribute_tree   ……ing👇"
    params = query_to_hash(dialog, data)
    ids = params["selection_ids"]

    # Check to see if we were passed a dictionary to pull, or determine
    # default behavior if nothing was passed.
    if params["dictionary"].to_s == "all_dictionaries"
      specific_dictionary = nil
    else
      specific_dictionary = second_if_empty(params["dictionary"],
                                            @dictionary_name)
    end

    # Construct our list of entities. If there is no active model, then there
    # can't be any entities.
    if Sketchup.active_model == nil
      entities = []
    elsif (ids.nil?)

      # If components are selected, then take those as our entities. If not,
      # take the current edit context.
      if Sketchup.active_model.selection.length > 0
        entities = []
        for entity in Sketchup.active_model.selection
          if entity.typename == "ComponentInstance" ||
             entity.typename == "Group"
            # Do not allow work against any components that
            # have a versioning error. (Meaning they are a version of
            # DCs that this plugin does not know how to handle.)
            if @dcobservers.has_version_error(entity) == false
              entities.push entity
            end
          end
        end
        if entities.length == 0 && Sketchup.active_model.active_path != nil
          entities = [Sketchup.active_model.active_path.pop]
        end
      elsif Sketchup.active_model.active_path != nil
        entities = [Sketchup.active_model.active_path.pop]
      end
    elsif ids.downcase == "active_model"
      entities = []
      entities.push Sketchup.active_model
    elsif ids.downcase == "selection"
      entities = Sketchup.active_model.selection
    else
      ids = ids.split(",")
      len = ids.length

      # TODO(thomthom) This is so badin so many ways. @entity_pointers is
      # tracking entities globally and never clears them. So it holds on to
      # stale data. I don't dare to clear then in the ModelObserver when a new
      # model is opened/created because that won't play nice with OSX's MDI.
      #
      # Additionally, it searches the selection and comparing strings - which is
      # dead slow - as with many other operations in DC.
      #
      # The real fix is a refactor, but we're days away from SU2014 M0 release
      # so as a quick and very dirty "fix" we prune the @entity_pointers cache
      # here so it won't raise errors with out template components. Their entity
      # ID is reused, so after the first model under Windows the cache will
      # point to deleted entities.
      @entity_pointers.delete_if { |key, value| value.deleted? }

      #puts 'pulling ids: ' + ids.to_s
      entities = ids.collect do |id|
        @entity_pointers[id] || Sketchup.active_model.selection.detect do |el|
          el.entityID.to_s == id
        end
      end
    end
    json = nil
    formulas = params["run_formulas"]

    deep = params["deep"]
    deep = false if deep.nil?

    # By default the current_depth is 0 and the max_recursion_depth is 1.
    current_depth = 0
    max_recursion_depth = 1
    if !params["max_recursion_depth"].nil?
      max_recursion_depth = params["max_recursion_depth"].to_i
    end

    attrs = []
    if entities != nil
      entities.each do |entity|
        next if entity.nil?
        next if entity.valid? == false
        next if entity.typename != "ComponentInstance" &&
                entity.typename != "Model" && entity.typename != "Group" &&
                entity.typename != "ComponentDefinition"

        entity = explode_if_3d_warehouse(entity)
        if (formulas == true)
          # Since the JS is no longer requesting individual formula results for
          # each refresh, run all of our formulas here, which will update
          # values and store any parser errors into the JSON data so errors
          # display
          run_all_formulas(entity)
        end
        # puts "json 👉 #{json}"
        attrs.push(entity_to_json(entity, deep, specific_dictionary, true,
                                  current_depth, max_recursion_depth))
      end
    end
    json = '{"entities": [' + attrs.join(",") + "]}"
    # puts "json 👉 #{json}"
    #puts 'pull_attribute_tree@return: ' + json
    js_callback(dialog, "pull_attribute_tree", data, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback
  # for any WebDialog created by the DynamicComponents class.
  #
  # Passes the entire dictionary of localizable strings to the WebDialog
  # using the Ruby/JS bridge
  #
  def pull_information(dialog, params)
    json = "{"
    json += "'version': '" + Sketchup.version.to_s + "'"
    json += ", 'language': '" + Sketchup.get_locale.to_s + "'"
    json += ", 'is_pro': " + Sketchup.is_pro?.to_s
    json += ", 'dc_version': '" + $dc_extension.version.to_s + "'"
    json += ", 'units': '" + get_default_units + "'"
    json += ", 'decimal_delimiter': '" + get_decimal_delimiter + "'"
    json += ", 'temp_path': '" + File.expand_path(get_temp_path) + "'"
    json += ", 'simplify': " + @simplify.to_s

    dc_help_url = Sketchup.get_datfile_info "DC_HELP",
                                            "http://help.sketchup.com/article/115543"

    dc_functions_url = Sketchup.get_datfile_info "DC_FUNCTIONS",
                                                 "http://help.sketchup.com/article/108144"

    dc_online_url = Sketchup.get_datfile_info "GENERATE_REPORT",
                                              "https://3dwarehouse.sketchup.com/generatereport.html"

    json += ", 'dc_help_url': '" + dc_help_url + "'"
    json += ", 'dc_functions_url': '" + dc_functions_url + "'"
    json += ", 'dc_online_url': '" + dc_online_url + "'"

    # Add information regarding default formatting.
    options = Sketchup.active_model.options["UnitsOptions"]
    json += ", 'length_format': '" + get_length_format + "'"
    json += ", 'length_unit': '" + get_length_unit + "'"
    json += ", 'length_precision': " + options["LengthPrecision"].to_s
    json += ", 'angle_precision': " + options["AnglePrecision"].to_s

    json += ", 'strings': " + "{"
    $dc_strings.GetStrings().keys.each do |string|
      json += '"' + clean_for_json(string) + '": "' +
              clean_for_json($dc_strings.GetString(string)) + '",'
    end
    json = json.chop + "}}"
    #puts(json)
    js_callback(dialog, "pull_information", params, json, nil)
  end

  # Gets the default length format option and returns as a string.
  #
  # Returns:
  # - string: A value in `architectural`, `engineering`, `decimal`,
  #           `fractional`, or ``.
  def get_length_format()
    options = Sketchup.active_model.options["UnitsOptions"]
    length_format = options["LengthFormat"]
    if length_format == Length::Architectural
      return "architectural"
    elsif length_format == Length::Engineering
      return "engineering"
    elsif length_format == Length::Decimal
      return "decimal"
    elsif length_format == Length::Fractional
      return "fractional"
    else
      return ""
    end
  end

  # Gets the default length unit option and returns as a string.
  #
  # Returns:
  # - string: A value in `inches`, `feet`, `millimeter`, `centimeter`, `meter`,
  #           or ``.
  def get_length_unit()
    options = Sketchup.active_model.options["UnitsOptions"]
    length_unit = options["LengthUnit"]
    if length_unit == Length::Inches
      return "inches"
    elsif length_unit == Length::Feet
      return "feet"
    elsif length_unit == Length::Millimeter
      return "millimeter"
    elsif length_unit == Length::Centimeter
      return "centimeter"
    elsif length_unit == Length::Meter
      return "meter"
    else
      return ""
    end
  end

  # Collects zero to N entity ids from the current selection and returns them
  # as a comma-separated list
  #
  # Returns:
  # - string: a JSON string whose key is 'selection_ids'.
  def pull_selection_ids(dialog, data)
    ids = []
    for entity in Sketchup.active_model.selection
      if entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
        ids.push entity.entityID.to_s
      end
    end
    json = '{selection_ids: "' + ids.join(",") + '"}'
    js_callback(dialog, "pull_selection_ids", data, json, nil)
    json
  end

  # Stores one or more attributes to one or more entities. This call supports
  # multi-entity selection/configuration operations for Dynamic Components.
  #
  def push_attribute_set(dialog, data)
    Sketchup.active_model.start_operation translate("Apply Attributes"), true
    params = query_to_hash(dialog, data)

    # If the author has exposed even a "live value" attribute to be editable
    # via a text box, then the values entered by an end user will be stored
    # in this @forced_config_values hash. This hash takes precedence over
    # any value or formula for the current redraw cycle. This hash will be
    # emptied at the end of the redraw() method.
    clear_forced_config_values()

    # phase one is getting our ids converted back into their related entities,
    # which should be found in the active selection
    ids = params["$ids"]
    return if ids.nil?

    # TODO(thomthom) See comment in pull_attribute_tree. I'm not sure if this is
    # needed, but this code follows the same pattern as another piece which
    # caused errors due to the hash holding on to invalid entities. To be
    # defensive I remove any deleted entities here as well.
    @entity_pointers.delete_if { |key, value| value.deleted? }

    ids = ids.split(",")
    entities = ids.collect do |id|
      @entity_pointers[id] || Sketchup.active_model.selection.detect do |el|
        el.entityID.to_s == id
      end
    end

    # next phase is to filter/update our parameter list for attributes, we
    # know for sure we should be getting [name]__value keys so collect those
    attrs = params.select do |key, value|
      key =~ /__value/
    end

    # If there are no DC attributes, we can skip some hoop jumping.
    any_entity_has_dc_attributes = false

    # next step is actually applying the attributes to valid components
    entities.each do |entity|
      # we don't process this call for non-component entities
      next if entity == nil || (entity.typename != "ComponentInstance" &&
                                entity.typename != "Group")

      # Determine if there are any dc attributes in this set.
      has_dc_attributes = false
      attrs.each do |key, value|
        name = key.split("__").first
        if name.include?(".") == false
          has_dc_attributes = true
          any_entity_has_dc_attributes = true
          break
        end
      end

      # If no lengthunits are set on our entity, then set them now.
      if has_dc_attributes && get_entity_lengthunits(entity, false) == nil
        default_length_units = get_entity_lengthunits(entity, true)
        set_attribute(entity, "_lengthunits", default_length_units)
      end

      attrs.each do |key, value|
        name = key.split("__").first

        # Classification attributes have a '.' in their name, as the
        # name represents the entire "path" to set the attribute.
        if name.include?(".")
          write_entity = entity
          if entity.is_a?(Sketchup::ComponentInstance)
            write_entity = entity.definition
          end

          # Classification values are stored in a sub-attribute called 'value'.
          path = name + ".value"
          set_nested_attribute(write_entity, path, value)
          next
        end

        if defined? entity.instance_variable_get(name)
          formula = params[name + "__formula"]
          label = params[name + "__label"]

          # If the user has pushed down a value but not a formula, then we
          # must wipe out any old formula that's been stored here. We also
          # store the value that was passed into @forced_config_values, so
          # that the next redraw knows to apply this value even if it's to
          # a "live value" attribute such as X or MATERIAL.
          if formula == nil
            set_forced_config_value(entity, name, value)
            formula = "ERASEFORMULA"
          end
          make_unique_if_needed(entity)
          set_attribute(entity, name, value, formula, label)
        end
      end
      # If this entity does not yet have an _name attribute, then set that
      # now so future formula references will not break.
      if has_dc_attributes && get_attribute_value(entity, "_name") == nil
        this_name = get_entity_formula_name(entity)
        set_attribute(entity, "_name", this_name)
      end
    end

    # Once everything has been updated we need to determine redraw/recompute
    # requirements, redraw the components, and commit the operation.
    if any_entity_has_dc_attributes
      if params["no_redraw"] == nil
        DCProgressBar::clear()
        entities.each do |entity|
          redraw(entity)
        end
        # If our last used tool was the scale tool then reselect the tool itself
        # to fire a redraw of the scale handles. This fixes a bug where the
        # handles can get out of sync on screen.
        if @dcobservers.last_tool_name == "ScaleTool"
          Sketchup.send_action "selectSelectionTool:"
          Sketchup.send_action "selectScaleTool:"
        end
        DCProgressBar::clear()
      else
        # Clear out our forced config hash so it doesn't accidentally apply to
        # the next redraw.
        clear_forced_config_values()
      end

      refresh_dialogs("Apply Attributes")
    end

    # Commit our operation.
    Sketchup.active_model.commit_operation

    js_callback(dialog, "push_attribute_set", data, nil, nil)
  end

  # A private method that returns the dictionary of templates.
  #
  def get_template_dictionary()
    model = Sketchup.active_model
    template_dict = model.attribute_dictionary(
      "dynamic_components_templates", true
    )
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # Passes the entire set of templates stored in this SketchUp model.
  #
  def read_templates(dialog, data)
    params = query_to_hash(dialog, data)
    template_dict = get_template_dictionary()

    # Return a JSON object with an `entries` property that points at an array.
    json = "{'entries':["
    template_dict.each { |key, value|
      json += "'" + clean_for_json(value) + "',"
    }

    # Remove the trailing comma from the JSON.
    if template_dict.length > 0
      json = json.chop
    end
    json += "]}"

    # Callback to the dialog.
    js_callback(dialog, "read_templates", data, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # Updates a specific template stored by the SketchUp model.
  #
  def update_template(dialog, data)
    params = query_to_hash(dialog, data)
    template_dict = get_template_dictionary()
    template_id = params["id"]

    # Look for the template in a `value` parameter first, then look for the
    # data in an input element in the web dialog.
    template_value = ""
    if params["value"].nil?
      if params["value_element_id"]
        template_value = dialog.get_element_value(params["value_element_id"])
      end
    else
      template_value = params["value"]
    end

    template_dict[template_id] = template_value
    json = '{"success": true, "id": "' + template_id + '"}'

    # Callback to the dialog.
    js_callback(dialog, "update_template", data, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # Deletes a specific template stored by the SketchUp model.
  #
  def delete_template(dialog, data)
    params = query_to_hash(dialog, data)
    template_dict = get_template_dictionary()
    template_id = params["id"]

    if template_dict[template_id].nil?
      json = '{"success": false, "message": "TEMPLATE_NOT_FOUND"}'
    else
      template_dict.delete_key(template_id)
      json = '{"success": true}'
    end

    # Callback to the dialog.
    js_callback(dialog, "delete_template", data, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # Saves a string to disk.
  #
  def save_report(dialog, data)
    params = query_to_hash(dialog, data)

    # Return an error if `filetype` is not found.
    filetype = params["filetype"]
    if filetype.nil?
      json = '{"success": false, ' +
             '"message": "Parameter filetype is missing.", ' +
             '"errorId": "MISSING_PARAMETER_FILETYPE"}'
      js_callback(dialog, "save_report", data, json, nil)
      return
    end

    # Return an error if `report_element_id` is not found.
    report_string_id = params["report_element_id"]
    if report_string_id.nil?
      json = '{"success": false, ' +
             '"message": "Parameter report_element_id is missing.", ' +
             '"errorId": "MISSING_PARAMETER_REPORT_ELEMENT_ID"}'
      js_callback(dialog, "save_report", data, json, nil)
      return
    end

    # Return an error if no report data is found.
    report_string = dialog.get_element_value(report_string_id)
    if report_string.nil?
      json = '{"success": false, ' +
             '"message": "No report data found.", ' +
             '"errorId": "NO_REPORT_DATA"}'
      js_callback(dialog, "save_report", data, json, nil)
      return
    end

    # Convert newlines if they were encoded.
    report_string = report_string.gsub("&#13;", '\n')
    save_report_to_file(report_string, filetype)

    # Callback to the dialog.
    json = '{"success": true}'
    js_callback(dialog, "save_report", data, json, nil)
  end
end
